<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Feed_Pubsubhubbub
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Subscriber.php 23775 2011-03-01 17:25:24Z ralph $
 */

/**
 * @see Zend_Feed_Pubsubhubbub
 */
require_once 'Zend/Feed/Pubsubhubbub.php';

/**
 * @see Zend_Date
 */
require_once 'Zend/Date.php';

/**
 * @category   Zend
 * @package    Zend_Feed_Pubsubhubbub
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Feed_Pubsubhubbub_Subscriber {
	/**
	 * An array of URLs for all Hub Servers to subscribe/unsubscribe.
	 *
	 * @var array
	 */
	protected $_hubUrls = array ();
	
	/**
	 * An array of optional parameters to be included in any
	 * (un)subscribe requests.
	 *
	 * @var array
	 */
	protected $_parameters = array ();
	
	/**
	 * The URL of the topic (Rss or Atom feed) which is the subject of
	 * our current intent to subscribe to/unsubscribe from updates from
	 * the currently configured Hub Servers.
	 *
	 * @var string
	 */
	protected $_topicUrl = '';
	
	/**
	 * The URL Hub Servers must use when communicating with this Subscriber
	 *
	 * @var string
	 */
	protected $_callbackUrl = '';
	
	/**
	 * The number of seconds for which the subscriber would like to have the
	 * subscription active. Defaults to null, i.e. not sent, to setup a
	 * permanent subscription if possible.
	 *
	 * @var int
	 */
	protected $_leaseSeconds = null;
	
	/**
	 * The preferred verification mode (sync or async). By default, this
	 * Subscriber prefers synchronous verification, but is considered
	 * desireable to support asynchronous verification if possible.
	 *
	 * Zend_Feed_Pubsubhubbub_Subscriber will always send both modes, whose
	 * order of occurance in the parameter list determines this preference.
	 *
	 * @var string
	 */
	protected $_preferredVerificationMode = Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC;
	
	/**
	 * An array of any errors including keys for 'response', 'hubUrl'.
	 * The response is the actual Zend_Http_Response object.
	 *
	 * @var array
	 */
	protected $_errors = array ();
	
	/**
	 * An array of Hub Server URLs for Hubs operating at this time in
	 * asynchronous verification mode.
	 *
	 * @var array
	 */
	protected $_asyncHubs = array ();
	
	/**
	 * An instance of Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface used to background
	 * save any verification tokens associated with a subscription or other.
	 *
	 * @var Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface
	 */
	protected $_storage = null;
	
	/**
	 * An array of authentication credentials for HTTP Basic Authentication
	 * if required by specific Hubs. The array is indexed by Hub Endpoint URI
	 * and the value is a simple array of the username and password to apply.
	 *
	 * @var array
	 */
	protected $_authentications = array ();
	
	/**
	 * Tells the Subscriber to append any subscription identifier to the path
	 * of the base Callback URL. E.g. an identifier "subkey1" would be added
	 * to the callback URL "http://www.example.com/callback" to create a subscription
	 * specific Callback URL of "http://www.example.com/callback/subkey1".
	 *
	 * This is required for all Hubs using the Pubsubhubbub 0.1 Specification.
	 * It should be manually intercepted and passed to the Callback class using
	 * Zend_Feed_Pubsubhubbub_Subscriber_Callback::setSubscriptionKey(). Will
	 * require a route in the form "callback/:subkey" to allow the parameter be
	 * retrieved from an action using the Zend_Controller_Action::_getParam()
	 * method.
	 *
	 * @var string
	 */
	protected $_usePathParameter = false;
	
	/**
	 * Constructor; accepts an array or Zend_Config instance to preset
	 * options for the Subscriber without calling all supported setter
	 * methods in turn.
	 *
	 * @param  array|Zend_Config $options Options array or Zend_Config instance
	 * @return void
	 */
	public function __construct($config = null) {
		if ($config !== null) {
			$this->setConfig ( $config );
		}
	}
	
	/**
	 * Process any injected configuration options
	 *
	 * @param  array|Zend_Config $options Options array or Zend_Config instance
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setConfig($config) {
		if ($config instanceof Zend_Config) {
			$config = $config->toArray ();
		} elseif (! is_array ( $config )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Array or Zend_Config object' . ' expected, got ' . gettype ( $config ) );
		}
		if (array_key_exists ( 'hubUrls', $config )) {
			$this->addHubUrls ( $config ['hubUrls'] );
		}
		if (array_key_exists ( 'callbackUrl', $config )) {
			$this->setCallbackUrl ( $config ['callbackUrl'] );
		}
		if (array_key_exists ( 'topicUrl', $config )) {
			$this->setTopicUrl ( $config ['topicUrl'] );
		}
		if (array_key_exists ( 'storage', $config )) {
			$this->setStorage ( $config ['storage'] );
		}
		if (array_key_exists ( 'leaseSeconds', $config )) {
			$this->setLeaseSeconds ( $config ['leaseSeconds'] );
		}
		if (array_key_exists ( 'parameters', $config )) {
			$this->setParameters ( $config ['parameters'] );
		}
		if (array_key_exists ( 'authentications', $config )) {
			$this->addAuthentications ( $config ['authentications'] );
		}
		if (array_key_exists ( 'usePathParameter', $config )) {
			$this->usePathParameter ( $config ['usePathParameter'] );
		}
		if (array_key_exists ( 'preferredVerificationMode', $config )) {
			$this->setPreferredVerificationMode ( $config ['preferredVerificationMode'] );
		}
		return $this;
	}
	
	/**
	 * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
	 * event will relate
	 *
	 * @param  string $url
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setTopicUrl($url) {
		if (empty ( $url ) || ! is_string ( $url ) || ! Zend_Uri::check ( $url )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "url"' . ' of "' . $url . '" must be a non-empty string and a valid' . ' URL' );
		}
		$this->_topicUrl = $url;
		return $this;
	}
	
	/**
	 * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
	 * event will relate
	 *
	 * @return string
	 */
	public function getTopicUrl() {
		if (empty ( $this->_topicUrl )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'A valid Topic (RSS or Atom' . ' feed) URL MUST be set before attempting any operation' );
		}
		return $this->_topicUrl;
	}
	
	/**
	 * Set the number of seconds for which any subscription will remain valid
	 *
	 * @param  int $seconds
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setLeaseSeconds($seconds) {
		$seconds = intval ( $seconds );
		if ($seconds <= 0) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Expected lease seconds' . ' must be an integer greater than zero' );
		}
		$this->_leaseSeconds = $seconds;
		return $this;
	}
	
	/**
	 * Get the number of lease seconds on subscriptions
	 *
	 * @return int
	 */
	public function getLeaseSeconds() {
		return $this->_leaseSeconds;
	}
	
	/**
	 * Set the callback URL to be used by Hub Servers when communicating with
	 * this Subscriber
	 *
	 * @param  string $url
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setCallbackUrl($url) {
		if (empty ( $url ) || ! is_string ( $url ) || ! Zend_Uri::check ( $url )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "url"' . ' of "' . $url . '" must be a non-empty string and a valid' . ' URL' );
		}
		$this->_callbackUrl = $url;
		return $this;
	}
	
	/**
	 * Get the callback URL to be used by Hub Servers when communicating with
	 * this Subscriber
	 *
	 * @return string
	 */
	public function getCallbackUrl() {
		if (empty ( $this->_callbackUrl )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'A valid Callback URL MUST be' . ' set before attempting any operation' );
		}
		return $this->_callbackUrl;
	}
	
	/**
	 * Set preferred verification mode (sync or async). By default, this
	 * Subscriber prefers synchronous verification, but does support
	 * asynchronous if that's the Hub Server's utilised mode.
	 *
	 * Zend_Feed_Pubsubhubbub_Subscriber will always send both modes, whose
	 * order of occurance in the parameter list determines this preference.
	 *
	 * @param  string $mode Should be 'sync' or 'async'
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setPreferredVerificationMode($mode) {
		if ($mode !== Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC && $mode !== Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_ASYNC) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid preferred' . ' mode specified: "' . $mode . '" but should be one of' . ' Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC or' . ' Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_ASYNC' );
		}
		$this->_preferredVerificationMode = $mode;
		return $this;
	}
	
	/**
	 * Get preferred verification mode (sync or async).
	 *
	 * @return string
	 */
	public function getPreferredVerificationMode() {
		return $this->_preferredVerificationMode;
	}
	
	/**
	 * Add a Hub Server URL supported by Publisher
	 *
	 * @param  string $url
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function addHubUrl($url) {
		if (empty ( $url ) || ! is_string ( $url ) || ! Zend_Uri::check ( $url )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "url"' . ' of "' . $url . '" must be a non-empty string and a valid' . ' URL' );
		}
		$this->_hubUrls [] = $url;
		return $this;
	}
	
	/**
	 * Add an array of Hub Server URLs supported by Publisher
	 *
	 * @param  array $urls
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function addHubUrls(array $urls) {
		foreach ( $urls as $url ) {
			$this->addHubUrl ( $url );
		}
		return $this;
	}
	
	/**
	 * Remove a Hub Server URL
	 *
	 * @param  string $url
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function removeHubUrl($url) {
		if (! in_array ( $url, $this->getHubUrls () )) {
			return $this;
		}
		$key = array_search ( $url, $this->_hubUrls );
		unset ( $this->_hubUrls [$key] );
		return $this;
	}
	
	/**
	 * Return an array of unique Hub Server URLs currently available
	 *
	 * @return array
	 */
	public function getHubUrls() {
		$this->_hubUrls = array_unique ( $this->_hubUrls );
		return $this->_hubUrls;
	}
	
	/**
	 * Add authentication credentials for a given URL
	 *
	 * @param  string $url
	 * @param  array $authentication
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function addAuthentication($url, array $authentication) {
		if (empty ( $url ) || ! is_string ( $url ) || ! Zend_Uri::check ( $url )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "url"' . ' of "' . $url . '" must be a non-empty string and a valid' . ' URL' );
		}
		$this->_authentications [$url] = $authentication;
		return $this;
	}
	
	/**
	 * Add authentication credentials for hub URLs
	 *
	 * @param  array $authentications
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function addAuthentications(array $authentications) {
		foreach ( $authentications as $url => $authentication ) {
			$this->addAuthentication ( $url, $authentication );
		}
		return $this;
	}
	
	/**
	 * Get all hub URL authentication credentials
	 *
	 * @return array
	 */
	public function getAuthentications() {
		return $this->_authentications;
	}
	
	/**
	 * Set flag indicating whether or not to use a path parameter
	 *
	 * @param  bool $bool
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function usePathParameter($bool = true) {
		$this->_usePathParameter = $bool;
		return $this;
	}
	
	/**
	 * Add an optional parameter to the (un)subscribe requests
	 *
	 * @param  string $name
	 * @param  string|null $value
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setParameter($name, $value = null) {
		if (is_array ( $name )) {
			$this->setParameters ( $name );
			return $this;
		}
		if (empty ( $name ) || ! is_string ( $name )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "name"' . ' of "' . $name . '" must be a non-empty string' );
		}
		if ($value === null) {
			$this->removeParameter ( $name );
			return $this;
		}
		if (empty ( $value ) || (! is_string ( $value ) && $value !== null)) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "value"' . ' of "' . $value . '" must be a non-empty string' );
		}
		$this->_parameters [$name] = $value;
		return $this;
	}
	
	/**
	 * Add an optional parameter to the (un)subscribe requests
	 *
	 * @param  string $name
	 * @param  string|null $value
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setParameters(array $parameters) {
		foreach ( $parameters as $name => $value ) {
			$this->setParameter ( $name, $value );
		}
		return $this;
	}
	
	/**
	 * Remove an optional parameter for the (un)subscribe requests
	 *
	 * @param  string $name
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function removeParameter($name) {
		if (empty ( $name ) || ! is_string ( $name )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid parameter "name"' . ' of "' . $name . '" must be a non-empty string' );
		}
		if (array_key_exists ( $name, $this->_parameters )) {
			unset ( $this->_parameters [$name] );
		}
		return $this;
	}
	
	/**
	 * Return an array of optional parameters for (un)subscribe requests
	 *
	 * @return array
	 */
	public function getParameters() {
		return $this->_parameters;
	}
	
	/**
	 * Sets an instance of Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface used to background
	 * save any verification tokens associated with a subscription or other.
	 *
	 * @param  Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface $storage
	 * @return Zend_Feed_Pubsubhubbub_Subscriber
	 */
	public function setStorage(Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface $storage) {
		$this->_storage = $storage;
		return $this;
	}
	
	/**
	 * Gets an instance of Zend_Feed_Pubsubhubbub_Storage_StorageInterface used
	 * to background save any verification tokens associated with a subscription
	 * or other.
	 *
	 * @return Zend_Feed_Pubsubhubbub_Model_SubscriptionInterface
	 */
	public function getStorage() {
		if ($this->_storage === null) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'No storage vehicle ' . 'has been set.' );
		}
		return $this->_storage;
	}
	
	/**
	 * Subscribe to one or more Hub Servers using the stored Hub URLs
	 * for the given Topic URL (RSS or Atom feed)
	 *
	 * @return void
	 */
	public function subscribeAll() {
		return $this->_doRequest ( 'subscribe' );
	}
	
	/**
	 * Unsubscribe from one or more Hub Servers using the stored Hub URLs
	 * for the given Topic URL (RSS or Atom feed)
	 *
	 * @return void
	 */
	public function unsubscribeAll() {
		return $this->_doRequest ( 'unsubscribe' );
	}
	
	/**
	 * Returns a boolean indicator of whether the notifications to Hub
	 * Servers were ALL successful. If even one failed, FALSE is returned.
	 *
	 * @return bool
	 */
	public function isSuccess() {
		if (count ( $this->_errors ) > 0) {
			return false;
		}
		return true;
	}
	
	/**
	 * Return an array of errors met from any failures, including keys:
	 * 'response' => the Zend_Http_Response object from the failure
	 * 'hubUrl' => the URL of the Hub Server whose notification failed
	 *
	 * @return array
	 */
	public function getErrors() {
		return $this->_errors;
	}
	
	/**
	 * Return an array of Hub Server URLs who returned a response indicating
	 * operation in Asynchronous Verification Mode, i.e. they will not confirm
	 * any (un)subscription immediately but at a later time (Hubs may be
	 * doing this as a batch process when load balancing)
	 *
	 * @return array
	 */
	public function getAsyncHubs() {
		return $this->_asyncHubs;
	}
	
	/**
	 * Executes an (un)subscribe request
	 *
	 * @param  string $mode
	 * @return void
	 */
	protected function _doRequest($mode) {
		$client = $this->_getHttpClient ();
		$hubs = $this->getHubUrls ();
		if (empty ( $hubs )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'No Hub Server URLs' . ' have been set so no subscriptions can be attempted' );
		}
		$this->_errors = array ();
		$this->_asyncHubs = array ();
		foreach ( $hubs as $url ) {
			if (array_key_exists ( $url, $this->_authentications )) {
				$auth = $this->_authentications [$url];
				$client->setAuth ( $auth [0], $auth [1] );
			}
			$client->setUri ( $url );
			$client->setRawData ( $this->_getRequestParameters ( $url, $mode ), 'application/x-www-form-urlencoded' );
			$response = $client->request ();
			if ($response->getStatus () !== 204 && $response->getStatus () !== 202) {
				$this->_errors [] = array ('response' => $response, 'hubUrl' => $url );
			/**
			 * At first I thought it was needed, but the backend storage will
			 * allow tracking async without any user interference. It's left
			 * here in case the user is interested in knowing what Hubs
			 * are using async verification modes so they may update Models and
			 * move these to asynchronous processes.
			 */
			} elseif ($response->getStatus () == 202) {
				$this->_asyncHubs [] = array ('response' => $response, 'hubUrl' => $url );
			}
		}
	}
	
	/**
	 * Get a basic prepared HTTP client for use
	 *
	 * @param  string $mode Must be "subscribe" or "unsubscribe"
	 * @return Zend_Http_Client
	 */
	protected function _getHttpClient() {
		$client = Zend_Feed_Pubsubhubbub::getHttpClient ();
		$client->setMethod ( Zend_Http_Client::POST );
		$client->setConfig ( array ('useragent' => 'Zend_Feed_Pubsubhubbub_Subscriber/' . Zend_Version::VERSION ) );
		return $client;
	}
	
	/**
	 * Return a list of standard protocol/optional parameters for addition to
	 * client's POST body that are specific to the current Hub Server URL
	 *
	 * @param  string $hubUrl
	 * @param  mode $hubUrl
	 * @return string
	 */
	protected function _getRequestParameters($hubUrl, $mode) {
		if (! in_array ( $mode, array ('subscribe', 'unsubscribe' ) )) {
			require_once 'Zend/Feed/Pubsubhubbub/Exception.php';
			throw new Zend_Feed_Pubsubhubbub_Exception ( 'Invalid mode specified: "' . $mode . '" which should have been "subscribe" or "unsubscribe"' );
		}
		
		$params = array ('hub.mode' => $mode, 'hub.topic' => $this->getTopicUrl () );
		
		if ($this->getPreferredVerificationMode () == Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC) {
			$vmodes = array (Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC, Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_ASYNC );
		} else {
			$vmodes = array (Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_ASYNC, Zend_Feed_Pubsubhubbub::VERIFICATION_MODE_SYNC );
		}
		$params ['hub.verify'] = array ();
		foreach ( $vmodes as $vmode ) {
			$params ['hub.verify'] [] = $vmode;
		}
		
		/**
		 * Establish a persistent verify_token and attach key to callback
		 * URL's path/querystring
		 */
		$key = $this->_generateSubscriptionKey ( $params, $hubUrl );
		$token = $this->_generateVerifyToken ();
		$params ['hub.verify_token'] = $token;
		
		// Note: query string only usable with PuSH 0.2 Hubs
		if (! $this->_usePathParameter) {
			$params ['hub.callback'] = $this->getCallbackUrl () . '?xhub.subscription=' . Zend_Feed_Pubsubhubbub::urlencode ( $key );
		} else {
			$params ['hub.callback'] = rtrim ( $this->getCallbackUrl (), '/' ) . '/' . Zend_Feed_Pubsubhubbub::urlencode ( $key );
		}
		if ($mode == 'subscribe' && $this->getLeaseSeconds () !== null) {
			$params ['hub.lease_seconds'] = $this->getLeaseSeconds ();
		}
		
		// hub.secret not currently supported
		$optParams = $this->getParameters ();
		foreach ( $optParams as $name => $value ) {
			$params [$name] = $value;
		}
		
		// store subscription to storage
		$now = new Zend_Date ();
		$expires = null;
		if (isset ( $params ['hub.lease_seconds'] )) {
			$expires = $now->add ( $params ['hub.lease_seconds'], Zend_Date::SECOND )->get ( 'yyyy-MM-dd HH:mm:ss' );
		}
		$data = array ('id' => $key, 'topic_url' => $params ['hub.topic'], 'hub_url' => $hubUrl, 'created_time' => $now->get ( 'yyyy-MM-dd HH:mm:ss' ), 'lease_seconds' => $expires, 'verify_token' => hash ( 'sha256', $params ['hub.verify_token'] ), 'secret' => null, 'expiration_time' => $expires, 'subscription_state' => Zend_Feed_Pubsubhubbub::SUBSCRIPTION_NOTVERIFIED );
		$this->getStorage ()->setSubscription ( $data );
		
		return $this->_toByteValueOrderedString ( $this->_urlEncode ( $params ) );
	}
	
	/**
	 * Simple helper to generate a verification token used in (un)subscribe
	 * requests to a Hub Server. Follows no particular method, which means
	 * it might be improved/changed in future.
	 *
	 * @param  string $hubUrl The Hub Server URL for which this token will apply
	 * @return string
	 */
	protected function _generateVerifyToken() {
		if (! empty ( $this->_testStaticToken )) {
			return $this->_testStaticToken;
		}
		return uniqid ( rand (), true ) . time ();
	}
	
	/**
	 * Simple helper to generate a verification token used in (un)subscribe
	 * requests to a Hub Server.
	 *
	 * @param  string $hubUrl The Hub Server URL for which this token will apply
	 * @return string
	 */
	protected function _generateSubscriptionKey(array $params, $hubUrl) {
		$keyBase = $params ['hub.topic'] . $hubUrl;
		$key = md5 ( $keyBase );
		return $key;
	}
	
	/**
	 * URL Encode an array of parameters
	 *
	 * @param  array $params
	 * @return array
	 */
	protected function _urlEncode(array $params) {
		$encoded = array ();
		foreach ( $params as $key => $value ) {
			if (is_array ( $value )) {
				$ekey = Zend_Feed_Pubsubhubbub::urlencode ( $key );
				$encoded [$ekey] = array ();
				foreach ( $value as $duplicateKey ) {
					$encoded [$ekey] [] = Zend_Feed_Pubsubhubbub::urlencode ( $duplicateKey );
				}
			} else {
				$encoded [Zend_Feed_Pubsubhubbub::urlencode ( $key )] = Zend_Feed_Pubsubhubbub::urlencode ( $value );
			}
		}
		return $encoded;
	}
	
	/**
	 * Order outgoing parameters
	 *
	 * @param  array $params
	 * @return array
	 */
	protected function _toByteValueOrderedString(array $params) {
		$return = array ();
		uksort ( $params, 'strnatcmp' );
		foreach ( $params as $key => $value ) {
			if (is_array ( $value )) {
				foreach ( $value as $keyduplicate ) {
					$return [] = $key . '=' . $keyduplicate;
				}
			} else {
				$return [] = $key . '=' . $value;
			}
		}
		return implode ( '&', $return );
	}
	
	/**
	 * This is STRICTLY for testing purposes only...
	 */
	protected $_testStaticToken = null;
	
	final public function setTestStaticToken($token) {
		$this->_testStaticToken = ( string ) $token;
	}
}
